Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / feature / connections / src / androidHostTest / kotlin / org / meshtastic / feature / connections / AndroidScannerViewModelBondingTest.kt

Displaying Raw • Download

feature/connections/src/androidHostTest/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModelBondingTest.kt bd2863243bab6eb213401d949839a2bc74dde7e2 (bd286324) Text, 9.42 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.connections

Tff7b72import T7ee787androidx.lifecycle.Lifecycle
Tff7b72import T7ee787androidx.lifecycle.LifecycleObserver
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.ExperimentalCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.test.resetMain
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787kotlinx.coroutines.test.setMain
Tff7b72import T7ee787org.jetbrains.compose.resources.getString
Tff7b72import T7ee787org.junit.runner.RunWith
Tff7b72import T7ee787org.meshtastic.core.network.repository.UsbRepository
Tff7b72import T7ee787org.meshtastic.core.resources.Res
Tff7b72import T7ee787org.meshtastic.core.resources.bonding_failed_retry
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleDevice
Tff7b72import T7ee787org.meshtastic.core.testing.failBondAfterRecording
Tff7b72import T7ee787org.meshtastic.core.testing.failBondWith
Tff7b72import T7ee787org.meshtastic.core.testing.failBondWithSecurityException
Tff7b72import T7ee787org.meshtastic.core.testing.runUntilSettled
Tff7b72import T7ee787org.meshtastic.feature.connections.model.DeviceListEntry
Tff7b72import T7ee787org.robolectric.RobolectricTestRunner
Tff7b72import T7ee787org.robolectric.RuntimeEnvironment
Tff7b72import T7ee787org.robolectric.annotation.Config
Tff7b72import T7ee787kotlin.test.AfterTest
Tff7b72import T7ee787kotlin.test.BeforeTest
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* Coverage for [AndroidScannerViewModel.requestBonding]: only a successful bond arms the transport. Bond failures
* surface a warning and wait for an explicit retry so the Android pairing flow does not immediately re-enter through
* transport-side bonding.
*
* Robolectric is used only because the class under test lives in `androidMain`; the bonding outcomes themselves are
* injected via [org.meshtastic.core.testing.FakeBluetoothRepository], keeping each assertion deterministic. The real
* `AndroidBluetoothRepository.bond()` branches are covered separately by `AndroidBluetoothRepositoryBondTest`.
*/
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tf0883e@RunWithTb4b4b4(Te6edf3RobolectricTestRunnerTff7b72::Te6edf3classTb4b4b4)
Tf0883e@ConfigTb4b4b4(Te6edf3sdk Tff7b72= Tff7b72[T79c0ff3T79c0ff4Tff7b72]Tb4b4b4)
Tff7b72class T56d364AndroidScannerViewModelBondingTest Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3mac Tff7b72= Ta5d6ff"Ta5d6ffAA:BB:CC:DD:EE:FFTa5d6ff"
Tff7b72private Tff7b72val Te6edf3expectedFullAddress Tff7b72= Ta5d6ff"Ta5d6ffxTffd700$Te6edf3macTa5d6ff"

Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3harnessTb4b4b4: Te6edf3ScannerViewModelHarness
Tff7b72private Tff7b72lateinit Tff7b72var Te6edf3viewModelTb4b4b4: Te6edf3AndroidScannerViewModel

Tf0883e@BeforeTest
Tff7b72fun Td2a8ffsetUpTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3harness Tff7b72= Te6edf3ScannerViewModelHarnessTb4b4b4(Tb4b4b4)
Te6edf3DispatchersTb4b4b4.Te6edf3setMainTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4)
Te6edf3viewModel Tff7b72=
Te6edf3AndroidScannerViewModelTb4b4b4(
Te6edf3serviceRepository Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3serviceRepositoryTb4b4b4,
Te6edf3radioController Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3radioControllerTb4b4b4,
Te6edf3radioInterfaceService Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3radioInterfaceServiceTb4b4b4,
Te6edf3radioPrefs Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3radioPrefsTb4b4b4,
Te6edf3recentAddressesDataSource Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3recentAddressesDataSourceTb4b4b4,
Te6edf3getDiscoveredDevicesUseCase Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3getDiscoveredDevicesUseCaseTb4b4b4,
Te6edf3networkRepository Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3networkRepositoryTb4b4b4,
Te6edf3dispatchers Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3dispatchersTb4b4b4,
Te6edf3bluetoothRepository Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4,
Te6edf3usbRepository Tff7b72= Te6edf3inertUsbRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3uiPrefs Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3uiPrefsTb4b4b4,
Te6edf3firmwareRecoveryDataSource Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3firmwareRecoveryDataSourceTb4b4b4,
Te6edf3bleScanner Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3bleScannerTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@AfterTest
Tff7b72fun Td2a8fftearDownTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Order matters: the ViewModel's coroutines must be gone before Main is unset.
Te6edf3harnessTb4b4b4.Te6edf3clearViewModelTb4b4b4(Te6edf3viewModelTb4b4b4)
Te6edf3DispatchersTb4b4b4.Te6edf3resetMainTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e/**
* A real [UsbRepository] (it is `final` and cross-module, so Mokkery can't mock it) made inert for this test: a
* DESTROYED [Lifecycle] means its `init` launch and eager `stateIn` never run, so its (cyclic) lazies are never
* evaluated. The USB path is irrelevant to bonding — the ViewModel only stores this collaborator.
*/
Tff7b72private Tff7b72fun Td2a8ffinertUsbRepositoryTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3UsbRepository Tff7b72= Te6edf3UsbRepositoryTb4b4b4(
Te6edf3application Tff7b72= Te6edf3RuntimeEnvironmentTb4b4b4.Te6edf3getApplicationTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3dispatchers Tff7b72= Te6edf3harnessTb4b4b4.Te6edf3dispatchersTb4b4b4,
Te6edf3processLifecycle Tff7b72= Te6edf3DestroyedLifecycleTb4b4b4,
Te6edf3usbBroadcastReceiverLazy Tff7b72= Te6edf3lazy Tb4b4b4{ Te6edf3errorTb4b4b4(Ta5d6ff"Ta5d6ffUsbBroadcastReceiver must not be used in this testTa5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4,
Te6edf3usbManagerLazy Tff7b72= Te6edf3lazy Tb4b4b4{ Tff7b72null Tb4b4b4}Tb4b4b4,
Te6edf3usbSerialProberLazy Tff7b72= Te6edf3lazy Tb4b4b4{ Te6edf3errorTb4b4b4(Ta5d6ff"Ta5d6ffUsbSerialProber must not be used in this testTa5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72object T56d364DestroyedLifecycle Tb4b4b4: Te6edf3LifecycleTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72override Tff7b72val Te6edf3currentStateTb4b4b4: Te6edf3State Tff7b72= Te6edf3StateTb4b4b4.Te6edf3DESTROYED

Tff7b72override Tff7b72fun Td2a8ffaddObserverTb4b4b4(Te6edf3observerTb4b4b4: Te6edf3LifecycleObserverTb4b4b4) Tff7b72= Tffa657Unit

Tff7b72override Tff7b72fun Td2a8ffremoveObserverTb4b4b4(Te6edf3observerTb4b4b4: Te6edf3LifecycleObserverTb4b4b4) Tff7b72= Tffa657Unit
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`successful bond arms the transport`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3ScannerViewModelHarnessTb4b4b4.Te6edf3unbondedBleEntryTb4b4b4(Te6edf3macTb4b4b4)Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3bondCallsTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3expectedFullAddressTb4b4b4, Te6edf3harnessTb4b4b4.Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`generic bond failure does not arm the transport and surfaces an error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// A failed pairing attempt should wait for another user action instead of immediately arming the transport,
T8b949e// which would call transport-side bond() and risk a duplicate PIN dialog or Android createBond() throttle.
Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3failBondWithTb4b4b4(Te6edf3ExceptionTb4b4b4(Ta5d6ff"Ta5d6ffFailed to initiate bondingTa5d6ff"Tb4b4b4)Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3ScannerViewModelHarnessTb4b4b4.Te6edf3unbondedBleEntryTb4b4b4(Te6edf3macTb4b4b4)Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3bondCallsTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Te6edf3runUntilSettled Tb4b4b4{ Te6edf3harnessTb4b4b4.Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Tff7b72null Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3getStringTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bonding_failed_retryTb4b4b4)Tb4b4b4, Te6edf3harnessTb4b4b4.Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`bond failure after Android records bond still arms the transport`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3failBondAfterRecordingTb4b4b4(Te6edf3ExceptionTb4b4b4(Ta5d6ff"Ta5d6ffTimed out waiting for bonding to completeTa5d6ff"Tb4b4b4)Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3ScannerViewModelHarnessTb4b4b4.Te6edf3unbondedBleEntryTb4b4b4(Te6edf3macTb4b4b4)Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3bondCallsTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3expectedFullAddressTb4b4b4, Te6edf3harnessTb4b4b4.Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`security exception does not arm the transport and surfaces an error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// Missing BLUETOOTH_CONNECT: connecting would fail the same way, so surface the error and do NOT arm.
Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3failBondWithSecurityExceptionTb4b4b4(Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3ScannerViewModelHarnessTb4b4b4.Te6edf3unbondedBleEntryTb4b4b4(Te6edf3macTb4b4b4)Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3bondCallsTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Te6edf3runUntilSettled Tb4b4b4{ Te6edf3harnessTb4b4b4.Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Tff7b72null Tb4b4b4}
Te6edf3assertNotNullTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`CancellationException from bond propagates without arming transport`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// CE must propagate — not be swallowed into an error message or transport arming.
T8b949e// Discriminator: if the CE catch were removed, CE would fall through to catch(Exception),
T8b949e// set an error message, and return false — errorMessage would be non-null, failing this test.
Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3failBondWithTb4b4b4(Te6edf3CancellationExceptionTb4b4b4(Ta5d6ff"Ta5d6ffcancelledTa5d6ff"Tb4b4b4)Tb4b4b4)

Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3ScannerViewModelHarnessTb4b4b4.Te6edf3unbondedBleEntryTb4b4b4(Te6edf3macTb4b4b4)Tb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertNullTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4, Ta5d6ff"Ta5d6fftransport must not be armedTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(
Te6edf3harnessTb4b4b4.Te6edf3serviceRepositoryTb4b4b4.Te6edf3errorMessageTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffCE must propagate, not be caught as generic failureTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`already bonded entry arms the transport without bonding`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
T8b949e// R6: selecting an already-bonded device connects directly without invoking createBond().
Tff7b72val Te6edf3bonded Tff7b72= Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTb4b4b4(Te6edf3device Tff7b72= Te6edf3FakeBleDeviceTb4b4b4(Te6edf3address Tff7b72= Te6edf3macTb4b4b4, Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffNodeTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3bonded Tff7b72= Tff7b72trueTb4b4b4)

Tff7b72val Te6edf3initiatedImmediately Tff7b72= Te6edf3viewModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3bondedTb4b4b4)
Te6edf3testSchedulerTb4b4b4.Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3initiatedImmediatelyTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3harnessTb4b4b4.Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3bondCallsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3expectedFullAddressTb4b4b4, Te6edf3harnessTb4b4b4.Te6edf3radioControllerTb4b4b4.Te6edf3lastSetDeviceAddressTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.06s